跳到主要内容

浏览器环境中渲染表格

· 阅读需 14 分钟

在网页中渲染一个表格,基本上是这两种方法,一是采用原生的table 标签,二是通过div 标签+contenteditable 属性来。

1. 原生 table 标签和 div 标签的对比

table 标签

原生 table 标签能够很方便的去渲染出表格,只需要按照 table 标签的规范来填充数据。

如果我们要修改它的样式,要通过 css 来修改每一个部分对应的标签的 css 属性。例如th,td,td标签等等。

div 实现(div 代指其他标签)

俗话说,万物皆可 div。只要我们渲染逻辑写的缜密,我们仍然可以去通过 div 去实现一套视觉友好,甚至能够符合 html5 的无障碍规范等等的表格组件出来。

如何选择

用 table 毕竟是已经封装好的原生的组件,所以难免会遇到一些不好解决的,具有局限性的配置等。就比如说,表格中每一个元素的边框,会产生一些重叠,导致内层的边框比外层的边框粗,但是我们可以通过 table 提供的border-collapse去实现内外边框粗细一致。还有我们想定制这个表格的元素的时候,会发现仍然有很多局限,因为每个元素都已经被规范应该是什么样的位置等,所以想要在元素中嵌入一些自定义的元素,就要花更多的事件去考虑需要怎么嵌入。

上述所说的问题在很多场景下没有大碍,但是在一些组件库中,或者在高度定制的表格项目中,我们用原生的 table 标签并不有利。所以在很多项目例如腾讯表格,没有用原生的 table 表格来实现,而是通过 div 等标签来实现的。

那么用 div 能有什么好处?首先上述原生 table 显露出来的一个问题就是,我们的渲染逻辑都局限在了 table 标签的规范中去了,所有的元素都被规范局限了渲染逻辑。很方便的渲染出一个表格,但问题总是在后期遇到。那么我们何不去封装一套更好的表格渲染逻辑,或者说是一套符合高度自定义的场景,同样暴露出和原生 table 标签所一致的 api 等等,但此基础上还暴露出了更多的自定义的 api,就像所说的嵌入元素等的 api。

2. div 的渲染设计

要实现一个表格,最终要的是渲染出表格的框架,其次就是实现表格的内容编辑的功能,后面就是对单元格的一些操作,以及适应屏幕的逻辑等。

1. 生成驱动视图的数据

表格,首先最容易想到的是用二维数组来实现,二维数组的第一维数据来渲染每一行,将每一行都渲染出来就形成了一个表格。

arr: [[ ,A,B,C],[1,A1,B1,C1],[2,A2,B2,C2]]

ABC
1A1B1C1
2A2B2C2

就像上面这样对应起来,arr 是最原始的渲染数组,其中包括了表头的数据。在 arr[0]中是[,A,B,C..],第一个是undefined,所以在后面的逻辑中渲染出来的是空白。是否一个表格数据应该包含表头数据?我认为最原始数组最好保留所有的数据,在后面的渲染中进行逻辑排除处理是一个比较好的做法。

2. 根据数据来渲染表格

数据已经被分为了数组的每一项,那么我们把每一维当行,每一维中的每一项当列即可。那么通过 2 层二维渲染就可以成功渲染出来。其中需要做的重要处理就是对空数据的处理,如果为空的时候我们不能单纯的不渲染这个元素,否则这个单元格的宽度和高度不会被撑起来。我们需要对它做标记,在 css 的处理上要针对性处理。

3. 编写表格的样式

如果我们单独为每个单元格都编写上下左右的边框,那就会出现边框重叠的问题,内部的宽度比外围的宽度粗,并且表格也出现了不对其的问题。

要解决边框粗细一致,在渲染的时候采取一定的措施,就是按一个方向去渲染表格的边框。比如说,只渲染表格的右边和下边的边框,最后渲染出来的结果就是没有任何的单元格边框被重叠,但是第一列和第一行的左边框或上边框没有被渲染出来。所以这个时候我们针对第一行和第一列,额外对它们设置左边和上边的边框的渲染。这样就达成了一个边框粗细一致的表格。

3.5 边框渲染出现的问题**

通过给每一个 div 单元格设置边框,因为第一行和第一列的边框渲染边框逻辑不一致,所以这些盒子在内容上的尺寸不一致。例如默认情况下每一个单元格的宽度是 100px,高度是 30px,边框宽度为 1px,那么大多数单元格边框渲染后的尺寸变成了 99px 和 29px,而第一行和第一列变成了 98px 和 28px。或许你会说,box-sizing可以让它们的内容尺寸保持一致,但是内容保持一致了盒子总体宽度还一致吗?显然不一致。 那么为了解决这一个问题,我运用到了伪元素来渲染表格边框。

4. 表格样式改良 - 伪元素

伪元素的一个特点就是不会占据文档流位置,不会对盒子产生任何的宽高上的形变等。用伪元素之后,我们的表格就可以简单的渲染成 100px,30px 的尺寸,而不考虑宽度。这时候这些单元格没有任何的边框,接下来要借助伪元素来渲染边框。通过伪元素渲染边框就可以简单的设置 100%宽高来让伪元素铺满 100px 和 30px 的 div,然后同样是按一个方向来渲染之前的表格,但是这一次的结果是渲染完表格之后,所有单元格的尺寸仍然是 100px 和 30px。

5. 实现内容的编辑**

在 div 中要实现内容的编辑,我们就需要借助contenteditable属性,设置了这个属性之后的 div 在被点击之后就会变成一个输入框,输入内容。

这个属性的编辑在生效后,我们要实现让编辑的最新的内容写入到 state 中,然后触发重新渲染。梳理一下完整的编辑逻辑就是:

1. 正常通过 state 最原始的空数组渲染出空白的单元格

2. 单元格被点击,编辑内容

3. 单元格上面的 input 事件被触发,回调函数中实现获取最新输入的内容,然后更新 state

4. state 中对应单元格索引的数据改变,最新渲染单元格中内容改变


看起来渲染逻辑没有问题,但是在第 3 点到第 4 点,也就是重新渲染的过程中,如果我们输入框一直在触发输入,渲染也在被一直重新触发,那么是不是会出现一个问题,就是我们输入的内容因为重新渲染而消失,导致数据丢失等? 而我出现的问题实际上是出现每次输入光标都会跑到最前面

为了解决这样的一个问题,就要让输入和渲染内容的容器被区分开,但是让几个 div 叠加到单元格上,点击单元格实际触发的是另外的一个输入框,但是这样单元格会变得层级比较复杂,可能会更容易出现 bug。我就想能不能用防抖来实现,也就是输入内容的时候并不立刻去重新渲染,而是等待一段时间。事实上,并不能行得通,因为如果在输入阶段停下来引起了防抖函数的执行,那么仍然会重新执行,光标仍然会跑到最前面。

参考了一些单元格的产品,我觉得在这上面做一个妥协也是行得通的,就是在单元格进行失焦的时候再来重新渲染。在后期也可以在数据的保护上更下功夫。

6. 实现单元格选中的高亮边框

实现高亮边框,修改单元格的伪元素的边框不太现实,这样也可能造成更多的元素的属性的修改,对性能可能不太有利。所以最好是单独用一个 div 来实现,而表格的尺寸和位置都通过元素的计算来实现。 比如我点击了一个 row 索引为 10,column 索引为 8 的单元格,那么根据这一个单元格索引结合每一个单元格的尺寸配置数据,来计算出单元格需要偏移多少个像素,宽度高度是多少。选中多个单元格也是如此,无非就是多计算几个单元格的尺寸。

7. 实现单元格的伸缩和虚拟列表适应全屏等

上面已经说明了这个表格是如何通过数据去驱动整个视图的渲染的,那么要做单元格的伸缩等,从 css 的渲染上入手即可。

根据一个配置,计算出这个配置对哪些索引的表格尺寸进行了修改,然后在注入 css 属性的时候根据这个计算进行动态的改变,我觉得这是一个比较开放的功能,没有很多需要特别强调的点,如果要进行优化性能,无非是针对大量数据的场景下表格尺寸计算的算法速度和空间占用等。

虚拟列表这些,也都是那样的方案,针对场景做做调整即可。